[fix/MAT-304-307-311] native 보안·딥링크·잡버그 묶음#311
Conversation
프로덕션 빌드에서 디바이스 FCM 토큰과 푸시 페이로드가 콘솔에 평문 출력되어 토큰 탈취 시 임의 푸시 알림 발송 위험. APNs/FCM 토큰 log, 등록 완료 log, foreground 메시지 페이로드 dump 모두 __DEV__ 가드로 감쌌다. 진단용 warn/error 는 유지. Refs: MAT-454
WebView 가 originWhitelist={['*']} 로 모든 origin 으로 navigation 을
허용해 오픈 리다이렉트에 취약. 번들 HTML 외 외부 navigation 을
file:// / about:blank / data: 만 허용하도록 좁히고, 추가로
onShouldStartLoadWithRequest 에서 블록리스트 외 요청을 명시적으로
차단해 defense-in-depth 적용. dev 빌드는 localhost 디버그 자산도
허용. CSS/JS subresource (jsdelivr KaTeX) 는 originWhitelist 의
적용 대상이 아니므로 정상 로드 유지.
Refs: MAT-455
jsdelivr CDN 으로 가져오는 katex CSS/JS 와 font-kopub CSS 가 변조 되어도 탐지 불가능했던 무결성 결함. 실제 자원에서 산출한 SHA-384 integrity 와 crossorigin="anonymous" 추가. KaTeX 0.16.9 / font-kopub 1.0 핀 버전 그대로 유지하므로 hash 갱신 트리거는 버전 bump 시점에 한정. Refs: MAT-456
PR-4 에서 useNativeOAuth 로 인앱 SDK 기반 소셜 로그인이 도입되며 url scheme 콜백 (exp:// / pointer://auth/callback) 흐름은 더 이상 도달 불가. 그대로 두면 getInitialURL().then 미catch 의 unhandled rejection 위험과 addEventListener 와의 race condition 만 남는다. hook 파일 삭제 + RootNavigator 호출 제거 + hooks barrel export 정리. 앱 내부 라우팅용 url scheme 처리 (예: 알림 딥링크) 는 useDeepLinkHandler 에서 별도 담당하므로 영향 없음. Refs: MAT-457
저사양 기기에서 콜드 스타트가 3s 를 넘으면 busy-wait polling 이
timeout 되어 알림 딥링크가 무시되던 문제. 100ms 간격 polling 을
NavigationContainer onReady fan-out 에 연결된 module-level
subscriber 로 교체하고, 기본 timeout 을 30s 로 확장.
navigationRef 자체의 addListener('state', ...) 는 attach 전에
호출하면 throw 하므로 App 의 NavigationContainer onReady prop 을
단일 ready 신호 지점으로 사용 (handleNavigationReady).
Refs: MAT-458
선언만 되고 어떤 subscription 도 할당되지 않은 채 cleanup 도 없는 notificationListener / responseListener ref 제거. expo-notifications 의 알림 응답 처리는 useDeepLinkHandler 가 담당하고 있어 중복으로 ref 를 채울 필요가 없다. Refs: MAT-459
handleSocialButtonPress 가 async 로 선언돼 Pressable onPress 에 Promise 를 반환했고, signInWithProvider 의 reject 가 unhandled rejection 으로 새어 나갈 수 있었다. 핸들러를 sync 로 바꾸고 void signInWithProvider(provider) 형태로 명시. 사용자 가시 에러는 useNativeOAuth 의 error state 로 이미 surface 되므로 silent fallback 우려 없음. Refs: MAT-460
string 입력 분기에서 JSON.parse 가 try/catch 없이 호출돼 malformed 콘텐츠가 들어오면 호출자 (ProblemViewer) 까지 그대로 throw 가 전파되어 화면 전체가 크래시되던 결함. parse 단계만 try/catch 로 감싸고 발생 detail 을 포함한 새 Error 로 재던져 디버깅을 쉽게 함. pointer-content-renderer 의 동명 함수 (MAT-451) 와 동일 패턴. Refs: MAT-461
빈 pointings 배열을 감지하던 console.warn 이 render 함수 내부에 있어 React pure render 원칙을 위반하고 매 렌더마다 동일한 경고가 중복 출력되는 노이즈 결함. 진단 로그가 실질적으로 가치를 제공하지 못해 단순 삭제 (Linear 와 사용자 결정). Refs: MAT-462
PR-7 lint pass 마무리. 사전 존재하던 unused import 제거.
Fast Refresh / NavigationContainer 재mount 시 onReady 가 재발화될 수 있어 clear() 를 하면 두 번째 mount 사이에 등록된 subscriber 가 손실된다. finish() 안에서 자기 자신을 delete 하므로 leak 없이 정리됨. OMC code-reviewer MEDIUM 후속 픽스.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
prettier/prettier — multi-line type import 가 100자 width 안에 들어가므로 single-line 으로 강제됨. CI lint fail 픽스.
There was a problem hiding this comment.
Pull request overview
학생 앱(네이티브)에서 보안 가드(WebView navigation 차단/FCM 로그 가드/CDN SRI)와 딥링크 콜드스타트 안정화, OAuth dead code 제거, 몇 가지 런타임 잡버그를 한 PR로 묶어 정리합니다.
Changes:
- Content WebView 보안 강화:
originWhitelist축소 +onShouldStartLoadWithRequest로 외부 navigation 차단, KaTeX/폰트 CDN에 SRI 추가, FCM 민감 로그__DEV__가드 - 딥링크 콜드스타트 안정화: navigation ready 대기 로직을 polling →
NavigationContainer.onReady기반 fan-out 대기로 전환 - dead code/잡버그 정리:
useSocialLoginCallback제거, LoginScreen unhandled rejection 방지, serializer JSON.parse 페일세이프, render body side-effect 제거
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/pointer-content-renderer/src/web/index.html | KaTeX/폰트 CDN 리소스에 SRI + crossorigin 추가 |
| packages/pointer-content-renderer/src/native/ContentWebView.tsx | originWhitelist 축소 및 navigation 차단 로직 추가 |
| apps/native/src/services/navigation/navigationRef.ts | navigation ready fan-out(waitForNavigationReady/handleNavigationReady) 추가 |
| apps/native/src/services/navigation/index.ts | 새 navigation 유틸 re-export |
| apps/native/src/navigation/RootNavigator.tsx | useSocialLoginCallback 호출 제거 |
| apps/native/src/hooks/useSocialLoginCallback.ts | (삭제) OAuth scheme callback 훅 제거 |
| apps/native/src/hooks/useFcmToken.ts | FCM/APNs 토큰 및 페이로드 로그를 __DEV__로 가드 |
| apps/native/src/hooks/useDeepLinkHandler.ts | navigation ready 대기를 waitForNavigationReady 기반으로 변경 |
| apps/native/src/hooks/index.ts | 배럴 export에서 useSocialLoginCallback 제거 |
| apps/native/src/features/student/problem/utils/serializeJSONToHTML.ts | string input JSON.parse 오류를 명시적 에러로 래핑 |
| apps/native/src/features/student/problem/screens/PointingScreen.tsx | render body의 console.warn side-effect 제거 |
| apps/native/src/features/auth/login/screens/LoginScreen.tsx | 소셜 로그인 호출을 void 처리해 unhandled rejection 방지 |
| apps/native/App.tsx | NavigationContainer에 onReady={handleNavigationReady} 연결 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Expo dev 빌드는 require('./*.html') 자산을 Metro bundler 가
http://<LAN-IP>:8081/... 로 서빙한다. originWhitelist 가 file:// 만 허용해
해당 URL 이 차단되었고, RN WebView 가 외부 핸들러로 fallback 하면서
Safari 가 열리고 컨텐츠가 보이지 않던 문제 수정.
- __DEV__ 일 때 originWhitelist 에 http://, https:// 포함
- onShouldStartLoadWithRequest 의 dev 예외를 모든 host 로 확장
- 프로덕션 빌드는 file:// + about:blank 만 허용 (보안 가드 유지)
PR-7 코드 리뷰 후속 픽스. 두 가지 우려 반영.
1. ContentWebViewHtmlSource 가 WebViewSource (외부 https uri 포함) 까지
허용해 API/실제 동작 (originWhitelist 가 file:// 만 허용) 이 불일치했다.
타입을 ImageRequireSource | { html: string } 으로 좁혀 의도와 일치시킴.
2. shouldAllowRequest 에서 data: URL 무조건 허용 → 임의 HTML/JS 실행
우회 경로가 됨. data: 분기 제거. RN WebView 의 injectedJavaScript /
postMessage 는 navigation 이 아니라 영향 없음.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…으로 일원화 PR-7 코드 리뷰 후속. originWhitelist 가 미스매치 시 외부 앱 (Linking) 으로 fallback 하는 RN WebView 동작 때문에, 이전 구현은 fail-open 경로가 있었다. - originWhitelist 를 ['*'] 로 두고 통과 layer 로만 사용 - onShouldStartLoadWithRequest 에서 deny-by-default 로 정책 강제 - file://, about:blank, dev Metro asset (port 8081 + /assets/*) 만 허용 - dev http(s) allow-all 도 함께 좁혀 외부 redirect 는 dev 에서도 차단됨
PR-7 코드 리뷰 후속 (P1). RootNavigator 가 sessionStatus 에 따라 단일 root screen (Splash | Auth | StudentApp) 만 등록하므로, 알림으로 종료 상태에서 앱이 켜질 때 sessionStatus === 'hydrating' 단계에서는 Splash 만 navigator 에 존재한다. 이때 navigationRef.isReady() 가 true 여도 'StudentApp' 으로의 CommonActions.navigate 가 silent no-op 이 되어 딥링크가 유실된다. 수정: - navigationRef 에 stateSubscribers + handleNavigationStateChange 추가, App.tsx 의 NavigationContainer onStateChange prop 에 연결. - waitForRouteRegistered(routeName, timeoutMs) helper 추가 — onReady + onStateChange 둘 다 구독해 원하는 route 가 등록될 때까지 대기. - useDeepLinkHandler 가 waitForNavigationReady 다음에 'StudentApp' 등록을 추가로 기다린 뒤 dispatch. - 곁들여 waitForNavigationReady 의 finish/handler/timer ordering 정리 (timer 를 let 으로 선언 후 할당 — 미래 편집 시 TDZ 위험 제거).
prettier — 100자 width 초과 문자열을 multi-line 으로 강제. CI format:check 픽스.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const studentAppReady = await waitForRouteRegistered('StudentApp'); | ||
| if (!studentAppReady) { | ||
| console.warn( | ||
| '[DeepLink] StudentApp route 미등록 — 알림이 unauthenticated 상태에 도착했을 가능성' | ||
| ); |
Summary
학생 앱의 보안 가드(FCM 로그/WebView navigation policy/CDN SRI), 딥링크 콜드스타트 안정성, 로그인·렌더·시리얼라이저 잡버그를 묶어 정리한다. 9개 sub-issue 가 한 PR.
__DEV__가드, WebVieworiginWhitelist는 외부 앱 fallback 방지를 위한 통과 layer 로 두고onShouldStartLoadWithRequest에서 deny-by-default navigation policy 강제, KaTeX/kopubbatang CDN 3개에 SHA-384 SRI + crossorigin.navigationRefbusy-wait polling (max 3s) 을 module-level subscriber Set +NavigationContainer.onReady/onStateChangefan-out 기반 이벤트 대기 (max 30s) 로 전환. auth hydration 후StudentApproute 등록까지 기다려 저사양 기기에서도 알림 손실 방지.useNativeOAuth도입 (PR-4 머지) 이후 사용되지 않는useSocialLoginCallback훅 + 라우터 호출 + 배럴 export 전부 삭제. 앱 내 화면 이동용 url scheme 핸들러는 보존.LoginScreen핸들러 unhandled rejection 차단(void),serializeJSONToHTMLJSON.parse 페일세이프,PointingScreenrender body 사이드이펙트(console.warn) 제거,useFcmToken미사용 listener ref 정리.Linear
__DEV__가드Changes
apps/native/src/hooks/useFcmToken.ts— APNs 토큰, FCM 토큰, 등록 완료 로그, 포그라운드 메시지 페이로드 4건을__DEV__가드. 진단성warn/error는 보존.packages/pointer-content-renderer/src/native/ContentWebView.tsx— RN WebView 의originWhitelist미매칭이 차단이 아니라Linking.openURLfallback 으로 이어지는 동작 때문에originWhitelist=['*']를 통과 layer 로 유지. 실제 navigation 정책은onShouldStartLoadWithRequest에서 deny-by-default 로 강제한다. 허용 대상은 productionfile:///about:blank, dev Metro 자산http(s)://*:8081/assets/...뿐이며,data:및 외부http(s)navigation 은 차단.packages/pointer-content-renderer/src/web/index.html—katex.min.css/katex.min.js/kopubbatang.min.css3개에integrity="sha384-..."+crossorigin="anonymous". defer +__katexLoaded/__katexFailed시그널 보존.useSocialLoginCallback.ts파일 삭제 +hooks/index.ts배럴 export +RootNavigator.tsx호출 제거. PR-4 의useNativeOAuth가 인앱 OAuth 를 처리하므로 url scheme callback 흐름은 더 이상 도달 불가.services/navigation/navigationRef.ts—readySubscribers/stateSubscribersfan-out,handleNavigationReady,handleNavigationStateChange,waitForNavigationReady(timeoutMs = 30_000),waitForRouteRegistered('StudentApp', timeoutMs = 30_000)추가.useDeepLinkHandler.ts의while폴링(3s) 을 이벤트 기반 대기로 교체하고, 콜드스타트 auth hydration 후StudentApproot route 등록까지 기다린 뒤 딥링크를 dispatch.useFcmToken.ts— 선언만 되고 미사용이던notificationListener/responseListenerref 두 개 제거.LoginScreen.tsx—handleSocialButtonPressasync → sync.void signInWithProvider(provider)로 unhandled rejection 차단. 에러는 이미 hook 의errorstate 로 화면에 노출 중.serializeJSONToHTML.ts— string input 일 때JSON.parse만 좁게 try/catch 로 감싸serializeJSONToHTML: invalid JSON input — <원인>형태로 throw.MAT-451(pointer-content-renderer) 와 동일 패턴.PointingScreen.tsx— render body 의if (pointings.length === 0) console.warn(...)사이드이펙트 제거.추가로 lint pass + post-review 후속 픽스:
useDeepLinkHandler.ts의 사전 존재하던 미사용useCallbackimport 제거.handleNavigationReady의clear()제거 (Fast Refresh / NavigationContainer 재mount 시 두 번째 onReady 사이 등록된 subscriber 손실 방지).http://<LAN-IP>:8081/assets/...로 resolve될 때 Safari 로 fallback 되는 문제를originWhitelist통과 layer +shouldAllowRequestdev Metro asset allow 로 해결.waitForNavigationReady/waitForRouteRegistered의 timer cleanup 을 optional guard 로 정리해 초기화 전 접근 가능성을 제거.Testing
pnpm lint --filter native— exit 0pnpm tsc --noEmit(apps/native, packages/pointer-content-renderer) — exit 0https://evil.example) 또는data:navigation 을 시도할 때 WebView 내부 이동/Safari fallback 없이 차단 + dev 콘솔에[ContentWebView] blocked navigation로그PointerContentView가 Safari 로 튕기지 않고 앱 안에서content.html렌더링Risk / Impact
useSocialLoginCallback삭제는 인앱 url scheme OAuth callback 경로를 끊지만, PR-4 머지 이후 도달 불가 경로. 앱 내 화면 이동용 url scheme (pointer://qna/<id>,pointer://publish/<id>) 은useDeepLinkHandler에서 그대로 처리.originWhitelist=['*']는 보안 허용 목록이 아니라 RN WebView 의 외부 앱 fallback 을 막기 위한 통과 layer. 실제 보안 경계는shouldAllowRequest의 deny-by-default navigation policy.ProblemViewer는 scrap 경로에서 아직 사용 중이며 이번 PR 에서는 추가 수정하지 않는다. 남은 사용처는 별도 branch 에서PointerContentView로 migration 예정.